/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { isESM } from "vs/base/common/amd"
import {
  AppResourcePath,
  FileAccess,
  nodeModulesAsarPath,
  nodeModulesPath,
} from "vs/base/common/network"
import * as platform from "vs/base/common/platform"
import { IProductConfiguration } from "vs/base/common/product"
import { URI } from "vs/base/common/uri"

class DefineCall {
  constructor(
    public readonly id: string | null | undefined,
    public readonly dependencies: string[] | null | undefined,
    public readonly callback: any,
  ) {}
}

class AMDModuleImporter {
  public static INSTANCE = new AMDModuleImporter()

  private readonly _isWebWorker =
    typeof self === "object" &&
    self.constructor &&
    self.constructor.name === "DedicatedWorkerGlobalScope"
  private readonly _isRenderer = typeof document === "object"

  private readonly _defineCalls: DefineCall[] = []
  private _initialized = false
  private _amdPolicy:
    | Pick<
        TrustedTypePolicy<{
          createScriptURL(value: string): string
        }>,
        "name" | "createScriptURL"
      >
    | undefined

  constructor() {}

  private _initialize(): void {
    if (this._initialized) {
      return
    }
    this._initialized = true
    ;(<any>globalThis).define = (id: any, dependencies: any, callback: any) => {
      if (typeof id !== "string") {
        callback = dependencies
        dependencies = id
        id = null
      }
      if (typeof dependencies !== "object" || !Array.isArray(dependencies)) {
        callback = dependencies
        dependencies = null
      }
      // if (!dependencies) {
      // 	dependencies = ['require', 'exports', 'module'];
      // }
      this._defineCalls.push(new DefineCall(id, dependencies, callback))
    }
    ;(<any>globalThis).define.amd = true

    if (this._isRenderer) {
      // eslint-disable-next-line no-restricted-globals
      this._amdPolicy = window.trustedTypes?.createPolicy("amdLoader", {
        createScriptURL(value) {
          // eslint-disable-next-line no-restricted-globals
          if (value.startsWith(window.location.origin)) {
            return value
          }
          if (value.startsWith("vscode-file://vscode-app")) {
            return value
          }
          throw new Error(`[trusted_script_src] Invalid script url: ${value}`)
        },
      })
    } else if (this._isWebWorker) {
      this._amdPolicy = (<any>globalThis).trustedTypes?.createPolicy(
        "amdLoader",
        {
          createScriptURL(value: string) {
            return value
          },
        },
      )
    }
  }

  public async load<T>(scriptSrc: string): Promise<T> {
    this._initialize()
    const defineCall = await (this._isWebWorker
      ? this._workerLoadScript(scriptSrc)
      : this._isRenderer
        ? this._rendererLoadScript(scriptSrc)
        : this._nodeJSLoadScript(scriptSrc))
    if (!defineCall) {
      throw new Error(`Did not receive a define call from script ${scriptSrc}`)
    }
    // TODO require, exports, module
    if (
      Array.isArray(defineCall.dependencies) &&
      defineCall.dependencies.length > 0
    ) {
      throw new Error(
        `Cannot resolve dependencies for script ${scriptSrc}. The dependencies are: ${defineCall.dependencies.join(", ")}`,
      )
    }
    if (typeof defineCall.callback === "function") {
      return defineCall.callback([])
    } else {
      return defineCall.callback
    }
  }

  private _rendererLoadScript(
    scriptSrc: string,
  ): Promise<DefineCall | undefined> {
    return new Promise<DefineCall | undefined>((resolve, reject) => {
      const scriptElement = document.createElement("script")
      scriptElement.setAttribute("async", "async")
      scriptElement.setAttribute("type", "text/javascript")

      const unbind = () => {
        scriptElement.removeEventListener("load", loadEventListener)
        scriptElement.removeEventListener("error", errorEventListener)
      }

      const loadEventListener = (e: any) => {
        unbind()
        resolve(this._defineCalls.pop())
      }

      const errorEventListener = (e: any) => {
        unbind()
        reject(e)
      }

      scriptElement.addEventListener("load", loadEventListener)
      scriptElement.addEventListener("error", errorEventListener)
      if (this._amdPolicy) {
        scriptSrc = this._amdPolicy.createScriptURL(scriptSrc) as any as string
      }
      scriptElement.setAttribute("src", scriptSrc)
      // eslint-disable-next-line no-restricted-globals
      window.document.getElementsByTagName("head")[0].appendChild(scriptElement)
    })
  }

  private _workerLoadScript(
    scriptSrc: string,
  ): Promise<DefineCall | undefined> {
    return new Promise<DefineCall | undefined>((resolve, reject) => {
      try {
        if (this._amdPolicy) {
          scriptSrc = this._amdPolicy.createScriptURL(
            scriptSrc,
          ) as any as string
        }
        importScripts(scriptSrc)
        resolve(this._defineCalls.pop())
      } catch (err) {
        reject(err)
      }
    })
  }

  private async _nodeJSLoadScript(
    scriptSrc: string,
  ): Promise<DefineCall | undefined> {
    try {
      const fs = <typeof import("fs")>globalThis._VSCODE_NODE_MODULES["fs"]
      const vm = <typeof import("vm")>globalThis._VSCODE_NODE_MODULES["vm"]
      const module = <typeof import("module")>(
        globalThis._VSCODE_NODE_MODULES["module"]
      )

      const filePath = URI.parse(scriptSrc).fsPath
      const content = fs.readFileSync(filePath).toString()
      const scriptSource = module.wrap(content.replace(/^#!.*/, ""))
      const script = new vm.Script(scriptSource)
      const compileWrapper = script.runInThisContext()
      compileWrapper.apply()
      return this._defineCalls.pop()
    } catch (error) {
      throw error
    }
  }
}

const cache = new Map<string, Promise<any>>()

let _paths: Record<string, string> = {}
if (typeof globalThis.require === "object") {
  _paths = (<Record<string, any>>globalThis.require).paths ?? {}
}

/**
 * Utility for importing an AMD node module. This util supports AMD and ESM contexts and should be used while the ESM adoption
 * is on its way.
 *
 * e.g. pass in `vscode-textmate/release/main.js`
 */
export async function importAMDNodeModule<T>(
  nodeModuleName: string,
  pathInsideNodeModule: string,
  isBuilt?: boolean,
): Promise<T> {
  if (isESM) {
    if (isBuilt === undefined) {
      const product =
        globalThis._VSCODE_PRODUCT_JSON as unknown as IProductConfiguration
      isBuilt = Boolean(
        (product ?? (<any>globalThis).vscode?.context?.configuration()?.product)
          ?.commit,
      )
    }

    if (_paths[nodeModuleName]) {
      nodeModuleName = _paths[nodeModuleName]
    }

    const nodeModulePath = `${nodeModuleName}/${pathInsideNodeModule}`
    if (cache.has(nodeModulePath)) {
      return cache.get(nodeModulePath)!
    }
    let scriptSrc: string
    if (/^\w[\w\d+.-]*:\/\//.test(nodeModulePath)) {
      // looks like a URL
      // bit of a special case for: src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts
      scriptSrc = nodeModulePath
    } else {
      const useASAR = isBuilt && !platform.isWeb
      const actualNodeModulesPath = useASAR
        ? nodeModulesAsarPath
        : nodeModulesPath
      const resourcePath: AppResourcePath = `${actualNodeModulesPath}/${nodeModulePath}`
      scriptSrc = FileAccess.asBrowserUri(resourcePath).toString(true)
    }
    const result = AMDModuleImporter.INSTANCE.load<T>(scriptSrc)
    cache.set(nodeModulePath, result)
    return result
  } else {
    return await import(nodeModuleName)
  }
}
